-
Notifications
You must be signed in to change notification settings - Fork 188
Add support for custom BIO method #1000
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
This adds support for using a custom BIO method for performing SSL/TLS I/O through a Ruby IO instance (normally the underlying socket). Alternatively, a pair of read and write procs may be used for performing the I/O.
|
I have a WIP branch in #736 which takes a similar approach of providing a custom
Wouldn't that convert all methods on
Some changes will be necessary there for proper error handling. Because Ruby exceptions are implemented using If the underlying BIO raises an exception, we must temporarily catch it, allow OpenSSL to clean up its internal state, wait for |
Indeed, this implementation would change the behavior to blocking, at least when using an IO-like object for the underlying BIO. However, for the custom BIO option (providing a read and a write proc), I have I understand the importance of supporting the non-blocking API. At the same time, having everything implemented as non-blocking is preventing other use cases, such as better integration with a fiber scheduler, or using io_uring for the actual I/O. Allowing a blocking BIO method is also an opportunity for achieving better performance, especially in conjunction with io_uring.
I have included a test case for raising an exception from within the BIO method, that seems to be working correctly. However, this may need more testing. https://github.com/ruby/openssl/pull/1000/changes#diff-9b8fe96fbb75f887aaa0cb69b982c886e041707496f37d8d6a5b443d92a5c347R2460
|
This PR adds support for using a custom BIO method for performing SSL/TLS I/O. It is an alternative to #736 and a possible fix for #731.
Summary
Currently, the openssl gem uses a socket BIO (over non-blocking sockets) that bypasses the Ruby I/O layer, except for calling
io_wait_readable/io_wait_writableto wait for I/O readiness. This prevents or makes it difficult to do encrypted I/O over alternative transports such as proxy connections (#731) or virtual sockets, for example in a testing situation.I've also been looking at providing a better way to integrate the openssl gem with a fiber scheduler and specifically being able to use it in conjunction with a low-level API for performing I/O using io_uring that I'm developing.
The aim of this PR is to provide a minimal API that allows setting a custom BIO method that either uses the stock
IO#readandIO#writemethods to perform I/O, or alternatively use custom procs to perform read and write operations, which will allow complete freedom for performing I/O using a low-level API, a proxy connection or any virtual interface.The proposed solution is based on the following design principles:
SSLSocketI/O methods inossl_ssl.c:#read,#write,#connect,#acceptetc.API
We add two methods:
SSLSocket#bio_method: get the socket's BIO method.SSLSocket#bio_method=: set the socket's BIO method.The setter method accepts the following:
nil: use the default socket BIOIO: use the given IO instance to perform I/O, via its#readand#writemethods. This will usually be the underlying socket object. Example usage:Object: use the given object to perform I/O, using the same interface as for an IO. Example usage:[read_proc, write_proc]: use the given pair of read/write procs to perform IO.The read proc takes an
IO::Bufferand a maximum read length, and should return the number of bytes read. The write proc takes anIO::Bufferand a write length, and should return the number of bytes written. Example usage:Implementation
Since we're just changing the BIO associated with the
SSLSocketinstance, we don't need to touch the I/O implementation in functions such asossl_ssl_read_internal. The custom BIO interface will never returnSSL_ERROR_WANT_READorSSL_ERROR_WANT_WRITE, and thus the I/O operation will complete immediately after the call to e.g.SSL_read.Since the custom BIO read and write hooks receive raw
char *buffers, we need to pass the buffer as eitherString(in the case ofIO/Objectmethod), orIO::Buffer(in the case of read/write procs). The advantage of using aIO::Bufferis that there's no need to copy data between the raw buffer and the string (or vice versa). Hopefully, in the future,IO#readandIO#writewould be able to accept anIO::Bufferas well asStringas buffer.Performance
A preliminary benchmark (source) shows a significant advantage to using a custom BIO method. This benchmarks measures the performance of the default socket BIO, the
IOcustom BIO method, and a custom BIO method using the UringMachine low-level API.Of course, the performance implications need to be investigated more thoroughly and may vary by OS, OpenSSL version, machine and network setup etc, and also concurrency.
OpenSSL and Ruby Compatibility
The implementation depends on the availability of
BIO_meth_newand associated functions, which were added in OpenSSL 1.1.0.The implementation also depends on the availability of the
IO::Bufferc API, namelyrb_io_buffer_new(available since Ruby 3.1) andrb_io_buffer_free_locked(available since Ruby 3.3).Future work
bio_methodkwarg toSSLSocket.new/SSLSocket.open(see also Addsync_closekwarg toSSLSocket.new#996).#readand#writemethods.cc @HoneyryderChuck @ioquatix @rhenium